package com.bytebuddytest.classvisitor;

import com.util.AnnotationInfo;
import com.util.ClassInfo;
import com.util.FieldInfo;
import com.util.MethodInfo;
import com.util.ParamInfo;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.annotation.AnnotationList;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.method.ParameterDescription;
import net.bytebuddy.description.method.ParameterList;
import net.bytebuddy.description.type.TypeDefinition;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.description.type.TypeList;
import net.bytebuddy.pool.TypePool;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author yutianhong
 * @version 1.0
 * @since 2023/12/20 19:30
 */
public class ClassResolver {

    private final Map<String, ClassInfo> cache = new HashMap<>();
    private final TypePool typePool;

    public ClassResolver(TypePool typePool) {
        this.typePool = typePool;
    }

    public Map<String, ClassInfo> doResolve(String className) {
        return scanClassName(className, new HashMap<>());
    }

    public Map<String, ClassInfo> scanClassName(String className, Map<String, ClassInfo> referenceClassInfo) {
        if (cache.get(className) != null) {
            return referenceClassInfo;
        }

        TypeDescription typeDescription = typePool.describe(className).resolve();

        ClassInfo classInfo = new ClassInfo();
        cache.put(className, classInfo);
        referenceClassInfo.put(className, classInfo);

        classInfo.className = className;
        classInfo.modifiers = typeDescription.getModifiers();
        classInfo.interfaceList = getInterfaces(typeDescription, referenceClassInfo);
        classInfo.typeDefinition = typeDescription.asGenericType();

        // TypeVariables
        TypeList.Generic typeVariables = typeDescription.getTypeVariables();
        classInfo.typeVariablesInfoList = getParamInfoList(typeVariables, referenceClassInfo);

        // super class
        TypeDescription.Generic superClass = typeDescription.getSuperClass();
        classInfo.superClassName = TypeUtils.getTypeNameWithoutGeneric(superClass);
        scanTypeDesc(superClass, referenceClassInfo);
        if (superClass != null && superClass.getSort().isParameterized()) {
            TypeList.Generic typeArguments = superClass.getTypeArguments();
            classInfo.superGenericInfoList = getParamInfoList(typeArguments, referenceClassInfo);
        }

        // annotation
        AnnotationList declaredAnnotations = typeDescription.getDeclaredAnnotations();
        classInfo.annotationInfoList = getAnnotationInfos(declaredAnnotations);

        // method
        MethodList<MethodDescription.InDefinedShape> declaredMethods = typeDescription.getDeclaredMethods();
        classInfo.methodInfoList = getMethodInfos(referenceClassInfo, declaredMethods);

        // field
        FieldList<FieldDescription.InDefinedShape> declaredFields = typeDescription.getDeclaredFields();
        classInfo.fieldInfoList = getFieldInfos(referenceClassInfo, declaredFields);

        return referenceClassInfo;
    }

    private List<String> getInterfaces(TypeDescription typeDescription, Map<String, ClassInfo> referenceClassInfo) {
        TypeList.Generic interfaces = typeDescription.getInterfaces();
        List<String> interfaceList = new ArrayList<>(interfaces.size());
        for (TypeDescription.Generic anInterface : interfaces) {
            String interfaceName = TypeUtils.getTypeNameWithoutGeneric(anInterface);
            interfaceList.add(interfaceName);
            scanTypeDesc(anInterface, referenceClassInfo);
        }
        return interfaceList;
    }

    private List<FieldInfo> getFieldInfos(Map<String, ClassInfo> referenceClassInfo, FieldList<FieldDescription.InDefinedShape> declaredFields) {
        List<FieldInfo> fieldInfos = new ArrayList<>(declaredFields.size());
        for (FieldDescription.InDefinedShape declaredField : declaredFields) {
            FieldInfo fieldInfo = new FieldInfo();
            fieldInfo.fieldName = declaredField.getActualName();
            fieldInfo.modifiers = declaredField.getActualModifiers();
            // annotation
            AnnotationList annotations = declaredField.getDeclaredAnnotations();
            fieldInfo.annotationInfoList = getAnnotationInfos(annotations);
            TypeDescription.Generic type = declaredField.getType();
            // resolve param info and copy to field
            ParamInfo paramInfo = getParamInfo(type, referenceClassInfo);
            fieldInfo.className = paramInfo.className;
            fieldInfo.genericType = paramInfo.genericType;
            fieldInfo.parameterized = paramInfo.parameterized;
            fieldInfo.typeVariable = paramInfo.typeVariable;
            fieldInfo.primitive = paramInfo.primitive;
            fieldInfo.array = paramInfo.array;
            fieldInfo.componentType = paramInfo.componentType;
            fieldInfo.genericInfoList = paramInfo.genericInfoList;
            fieldInfo.annotationInfoList = paramInfo.annotationInfoList;
            fieldInfos.add(fieldInfo);
            scanTypeDesc(declaredField.getType(), referenceClassInfo);
        }
        return fieldInfos;
    }

    private List<MethodInfo> getMethodInfos(Map<String, ClassInfo> referenceClassInfo, MethodList<MethodDescription.InDefinedShape> declaredMethods) {
        List<MethodInfo> methodInfos = new ArrayList<>(declaredMethods.size());
        for (MethodDescription.InDefinedShape declaredMethod : declaredMethods) {
            MethodInfo methodInfo = new MethodInfo();
            methodInfo.methodName = declaredMethod.getName();
            methodInfo.modifiers = declaredMethod.getModifiers();
            methodInfo.exceptionList = declaredMethod.getExceptionTypes()
                    .stream().map(TypeDefinition::getTypeName).collect(Collectors.toList());
            // annotation
            AnnotationList declaredAnnotations = declaredMethod.getDeclaredAnnotations();
            methodInfo.annotationInfoList = getAnnotationInfos(declaredAnnotations);
            // type variables
            TypeList.Generic methodTypeVariables = declaredMethod.getTypeVariables();
            methodInfo.typeVariablesInfoList = getParamInfoList(methodTypeVariables, referenceClassInfo);
            // return
            TypeDescription.Generic returnType = declaredMethod.getReturnType();
            methodInfo.outParam = getParamInfo(returnType, referenceClassInfo);
            // parameters
            ParameterList<ParameterDescription.InDefinedShape> parameters = declaredMethod.getParameters();
            List<ParamInfo> paramInfos = new ArrayList<>(parameters.size());
            for (int i = 0; i < parameters.size(); i++) {
                ParamInfo paramInfo = getParamInfo(parameters.get(i).getType(), referenceClassInfo);
                paramInfo.id = i;
                paramInfo.paramName = parameters.get(i).getName();
                // annotation
                AnnotationList annotations = parameters.get(i).getDeclaredAnnotations();
                paramInfo.annotationInfoList = getAnnotationInfos(annotations);
                paramInfos.add(paramInfo);
            }
            methodInfo.inParam = paramInfos;

            methodInfos.add(methodInfo);
        }
        return methodInfos;
    }

    private List<AnnotationInfo> getAnnotationInfos(AnnotationList declaredAnnotations) {
        List<AnnotationInfo> annotationInfos = new ArrayList<>(declaredAnnotations.size());
        for (AnnotationDescription declaredAnnotation : declaredAnnotations) {
            AnnotationInfo annotationInfo = new AnnotationInfo();
            TypeDescription annotationType = declaredAnnotation.getAnnotationType();
            annotationInfo.className = TypeUtils.getTypeNameWithoutGeneric(annotationType.asGenericType());
            // collect values
            MethodList<MethodDescription.InDefinedShape> declaredMethods = annotationType.getDeclaredMethods();
            Map<String, Object> map = new HashMap<>(declaredMethods.size());
            for (MethodDescription.InDefinedShape method : declaredMethods) {
                String key = method.getName();
                Object value = declaredAnnotation.getValue(method).resolve();
                map.put(key, value);
            }
            annotationInfo.values = map;

            annotationInfos.add(annotationInfo);
        }
        return annotationInfos;
    }

    private void scanTypeDesc(TypeDescription.Generic type, Map<String, ClassInfo> referenceClassInfo) {
        if (type == null || type.isPrimitive()) {
            return;
        }
        if (type.getSort().isWildcard() || type.getSort().isTypeVariable()) {
            for (TypeDescription.Generic upperBound : type.getUpperBounds()) {
                scanTypeDesc(upperBound, referenceClassInfo);
            }
            if (type.getSort().isWildcard()) {
                TypeList.Generic lowerBounds = type.getLowerBounds();
                for (TypeDescription.Generic lowerBound : lowerBounds) {
                    scanTypeDesc(lowerBound, referenceClassInfo);
                }
            }
            return;
        }
        if (type.isArray()) {
            scanTypeDesc(type.getComponentType(), referenceClassInfo);
            return;
        }
        if (type.getSort().isParameterized()) {
            for (TypeDescription.Generic typeArgument : type.getTypeArguments()) {
                scanTypeDesc(typeArgument, referenceClassInfo);
            }
        }
        // scan type itself
        String className = TypeUtils.getTypeNameWithoutGeneric(type);
        if (!TypeUtils.loadableByExtClassLoader(className)) {
            scanClassName(className, referenceClassInfo);
        }
    }

    private ParamInfo getParamInfo(TypeDescription.Generic typeDef, Map<String, ClassInfo> referenceClassInfo) {
        ParamInfo paramInfo = new ParamInfo();
        paramInfo.id = 0;
        paramInfo.className = TypeUtils.getTypeNameWithoutGeneric(typeDef);
        paramInfo.primitive = typeDef.isPrimitive();
        paramInfo.voidType = "void".equals(paramInfo.className);
        // generic
        paramInfo.parameterized = typeDef.getSort().isParameterized();
        paramInfo.genericType = TypeUtils.getGenericTypeString(typeDef);
        paramInfo.genericInfoList = getGenericInfoList(typeDef, referenceClassInfo);
        // typeVariable and wildcard
        paramInfo.typeVariable = typeDef.getSort().isTypeVariable();
        paramInfo.wildcard = typeDef.getSort().isWildcard();
        if (paramInfo.wildcard) {
            paramInfo.lowerBounds = typeDef.getLowerBounds().stream()
                    .map(e -> getParamInfo(e, referenceClassInfo))
                    .collect(Collectors.toList());
        }
        if (paramInfo.wildcard || paramInfo.typeVariable) {
            paramInfo.upperBound = typeDef.getUpperBounds().stream()
                    .map(e -> getParamInfo(e, referenceClassInfo))
                    .collect(Collectors.toList());
        }
        // array
        paramInfo.array = typeDef.isArray();
        if (paramInfo.array) {
            TypeDescription.Generic componentType = typeDef.getComponentType();
            if (componentType != null) {
                paramInfo.componentType = getParamInfo(componentType, referenceClassInfo);
                scanTypeDesc(componentType, referenceClassInfo);
            }
        }

        scanTypeDesc(typeDef, referenceClassInfo);
        return paramInfo;
    }

    private List<ParamInfo> getGenericInfoList(TypeDescription.Generic typeDef, Map<String, ClassInfo> referenceClassInfo) {
        if (!typeDef.getSort().isParameterized()) {
            return Collections.emptyList();
        }
        // parameters
        TypeList.Generic typeVariables = typeDef.getTypeArguments();
        return getParamInfoList(typeVariables, referenceClassInfo);
    }

    private List<ParamInfo> getParamInfoList(TypeList.Generic typeVariables, Map<String, ClassInfo> referenceClassInfo) {
        List<ParamInfo> genericList = new ArrayList<>(typeVariables.size());
        for (int i = 0; i < typeVariables.size(); i++) {
            TypeDescription.Generic typeVariable = typeVariables.get(i);
            ParamInfo paramInfo = getParamInfo(typeVariable, referenceClassInfo);
            paramInfo.id = i;
            genericList.add(paramInfo);
        }
        return genericList;
    }

}
